from __future__ import with_statement

import base64
import codecs
import copy
import cPickle as pickle
import glob
import hashlib
import logging
import os
import re
import shutil
import signal
import subprocess
import sys
import tempfile
import thread
import time
import traceback

sys.path.append("other/woodpecker")
import cssparser
import scsscompiler
import scssimporter

try:
    import json
except ImportError:
    import simplejson as json

logging.basicConfig(level = logging.DEBUG)

from optparse import OptionParser

DEFAULT_LOCALE = "en_US"

ALL_LOCALES = ["en_US", "nl_NL"]

TITLE = "Hyves"
DOMAIN = "hyves.nl"

htmlEscapeTable = {
    "&": "&amp;",
    '"': "&quot;",
    "'": "&apos;",
    ">": "&gt;",
    "<": "&lt;",
}

def htmlEscape(text):
    """Produce entities within text."""
    return "".join(htmlEscapeTable.get(c, c) for c in text)

extensionToMimeTypeMap = {
    "css": "text/css",
    "gif": "image/gif",
    "ico": "image/x-icon",
    "jpg": "image/jpeg",
    "jpeg": "image/jpeg",
    "js": "text/javascript",
    "png": "image/png"
}

def mimeTypeForExtension(extension):
    if extension[0] == ".":
        extension = extension[1:]
    extension = extension.lower()
    if extension in extensionToMimeTypeMap:
        return extensionToMimeTypeMap[extension]
    else:
        raise BuildError("Cannot determine MIME type for extension %s" % extension)

def dirEntries(path):
    if not os.path.exists(path):
        return
    for entry in sorted(os.listdir(path)):
        if entry[0] == ".":
            continue
        yield entry

def base64EncodeImage(path):
    (baseName, extension) = os.path.splitext(path)
    with open(path) as f:
        b64Image = "data:%s;base64,%s" % (mimeTypeForExtension(extension), base64.b64encode(f.read()))
    return b64Image

class BuildError(Exception):
    pass

def stopWatcher(signum, frame):
    print "\nKill 'em all!"
    os.kill() # it's funny because... calling kill() without arguments is a sure
              # way to raise an exception which appears to be the most reliable
              # way to kill ourselves

# TODO: Wrap all essential objects in a single JS file
class Builder(object):
    def __init__(self, options):
        self.options = options
        self.oldModules = {}
        self.modules = {}
        self.resources = {}
        self.debugResources = {}
        self.currentLocale = None
        self.verbose = True

        self.tempBuildDir = self.options.tempBuildDir if self.options.tempBuildDir else tempfile.mkdtemp(".build", "hybrid.")
        self.targetDir = os.path.join(self.tempBuildDir, ".build.tmp")
        self.finalTargetDir = self.tempBuildDir if self.options.serveFromVm else os.path.join(self.tempBuildDir, "build")

        scssimporter.Importer.addPath("other/woodpecker/stylesheets")

    def log(self, message, level = logging.INFO):
        if self.verbose:
            logging.log(level, message)

    def build(self):
        # make sure these are reset for every subsequent build
        self.modules = {}
        self.resources = {}
        self.debugResources = {}

        if os.path.exists(self.targetDir):
            self.log("Removing old build dir")
            os.system("rm -rf %s" % self.targetDir)

        if not os.path.exists(self.targetDir):
            self.log("Creating build dir: %s" % self.targetDir)
            os.mkdir(self.targetDir)

        exception = None
        for locale in ALL_LOCALES:
            self.currentLocale = locale

            try:
                self.modules = {}
                for moduleName in dirEntries("modules"):
                    if moduleName in self.modules:
                        continue
                    self.packageModule(moduleName)
                self.packageModule("boot", keepInline = True)

                bootPath, bootHash, jsonBootPath, jsonBootHash = self.processBootHtml()
                if self.options.includeDebugFiles:
                    debugBootPath, debugBootHash, debugJsonBootPath, debugJsonBootHash = self.processBootHtml(None, True)

                self.oldModules[locale] = copy.copy(self.modules)
            except Exception, exception:
                logging.exception(exception)
                bootPath, bootHash, jsonBootPath, jsonBootHash = self.processBootHtml(exception)
                if self.options.includeDebugFiles:
                    debugBootPath, debugBootHash, debugJsonBootPath, debugJsonBootHash = self.processBootHtml(exception, True)
                break

            self.writeVersionFile("__version__", bootHash)
            self.writeVersionFile("__versionjson__", jsonBootHash)

            if self.options.includeDebugFiles:
                self.writeVersionFile("__version__", debugBootHash, True)
                self.writeVersionFile("__versionjson__", debugJsonBootHash, True)

        self.log("Moving build %s to final dir: %s" % (self.targetDir, self.finalTargetDir))
        if os.path.exists(self.finalTargetDir):
            os.system("rm -rf %s/*" % self.finalTargetDir)
        else:
            os.system("mkdir %s" % self.finalTargetDir)
        os.system("mv %s/* %s" % (self.targetDir, self.finalTargetDir))

        if self.options.createZipBuild:
            self.zipBuild()

        if exception is not None:
            raise exception

    def packageModule(self, moduleName, keepInline = False):

        if moduleName in self.modules and not keepInline:
            return # module already packaged

        # load module from manifest
        try:
            with open("modules/" + moduleName + "/manifest.json", "r") as f:
                contents = f.read()

            manifest = json.loads(contents)
        except Exception, exception:
            raise BuildError("Error loading manifest file for module %s: %s" % (moduleName, str(exception)))

        if not "namespace" in manifest or not "dependencies" in manifest:
            raise BuildError("Module %s has incomplete manifest (missing namespace or dependencies?)" % moduleName)

        module = {}
        module["name"] = moduleName
        module["namespace"] = manifest["namespace"]
        module["dependencies"] = manifest["dependencies"]
        module["essential"] = manifest["essential"] if "essential" in manifest else False
        module["excludeFromDistribution"] = manifest["excludeFromDistribution"] if "excludeFromDistribution" in manifest else False
        module["dir"] = "modules/" + moduleName
        module["js"] = manifest["sources"] if "sources" in manifest else []
        module["css"] = manifest["styles"] if "styles" in manifest else []
        module["statics"] = manifest["statics"] if "statics" in manifest else []
        module["keepInline"] = keepInline

        module["tmpl"] = []
        for path in dirEntries(module["dir"] + "/tmpl"):
            module["tmpl"].append({ "path": path })

        module["i18n"] = []
        for path in dirEntries(module["dir"] + "/i18n"):
            module["i18n"].append({ "path": path })

        if not self.options.serve and module["excludeFromDistribution"]:
            self.log("Excluding module %s from distrubtion build" % moduleName)
            return # if we are making a build we want to exclude this module

        # package prerequisite modules first
        for prerequisite in module["dependencies"]:
            self.packageModule(prerequisite)

        self.log("Packaging module %s %s for locale %s..." % (moduleName, "(inline version)" if keepInline else "", self.currentLocale))

        self.modules[moduleName + "-inline" if keepInline else moduleName] = module

        self.loadCachedModule(self.currentLocale, moduleName, module)

        self.compileAndLoadCss(module)

        self.loadJavaScript(moduleName, module)

        self.loadTemplates(moduleName, module)

        self.loadTranslations(module)

        self.copyStatics(module)

        if self.options.includeDebugFiles:
            # will contain unminified js with debug statements
            self.debugResources[moduleName] = {}
            self.debugResources[moduleName][self.currentLocale] = self.packageJavascript(moduleName, module, True)

        if keepInline:
            self.addResource(moduleName + "-inline", self.packageJavascript(moduleName, module))
        else:
            self.addResource(moduleName, self.packageJavascript(moduleName, module))

    def loadCachedModule(self, locale, moduleName, module):
        if self.options.jslint:
            return # we can't perform a jslint check if caching is enabled

        if module["keepInline"]:
            moduleName += "-inline"

        if not locale in self.oldModules:
            self.oldModules[locale] = {}
        if moduleName in self.oldModules[locale]:
            cachedModule = self.oldModules[locale][moduleName]
        else:
            cachedModule = module # this will initialize the caching timestamp sooner

        if (cachedModule["namespace"] != module["namespace"] or
            cachedModule["dependencies"] != module["dependencies"]):
            return

        self.loadCachedDir(module, cachedModule, "js", [ "jsContent", "inlineJs" ], [ "js" ])
        self.loadCachedDir(module, cachedModule, "css", [ "cssContent", "scssScope" ], [ "css" ])
        self.loadCachedDir(module, cachedModule, "i18n", [ "translations", "allTranslations" ], [ "i18n" ])
        self.loadCachedDir(module, cachedModule, "tmpl", [ "templates" ], [ "i18n" ])
        self.loadCachedDir(module, cachedModule, "statics", [ "staticMap" ])

    def loadCachedDir(self, targetModule, cachedModule, dirName, keys, dependencies = []):
        timestampKey = dirName + "Timestamp"
        moduleDir = "modules/" + targetModule["name"] + "/" + dirName
        if timestampKey in cachedModule:
            upToDate = True
            for resource in targetModule[dirName]:
                path = moduleDir + "/" + resource["path"]
                if not os.path.exists(path):
                    upToDate = False
                    continue # don't break, or else timestamps might not update
                mtime = os.stat(path).st_mtime
                if mtime > cachedModule[timestampKey]:
                    upToDate = False
                    cachedModule[timestampKey] = mtime

            for dependency in dependencies:
                for prerequisite in [targetModule["name"]] + targetModule["dependencies"]:
                    if prerequisite == targetModule["name"] and dependency == dirName:
                        continue
                    dependencyTimestampKey = dependency + "Timestamp"
                    if not dependencyTimestampKey in self.modules[prerequisite]:
                        continue
                    if self.modules[prerequisite][dependencyTimestampKey] > cachedModule[timestampKey]:
                        upToDate = False
                        cachedModule[timestampKey] = self.modules[prerequisite][dependencyTimestampKey]

            targetModule[timestampKey] = cachedModule[timestampKey]
        else:
            upToDate = False
            if os.path.exists(moduleDir):
                targetModule[timestampKey] = os.stat(moduleDir).st_mtime
            else:
                targetModule[timestampKey] = None

        if upToDate:
            for key in keys:
                targetModule[key] = cachedModule[key]

    def compileAndLoadCss(self, module):
        if "cssContent" in module:
            if module["cssContent"] != "":
                self.log("  Using cached CSS...")
            return

        scssContent = u""
        for style in module["css"]:
            path = module["dir"] + "/css/" + style["path"]
            with open(path) as f:
                scssContent += f.read()
        if scssContent == "":
            module["cssContent"] = ""
            module["scssScope"] = None
            return

        self.log("  Compiling CSS...")

        # perform the actual compilation
        parser = cssparser.CSSParser()
        options = cssparser.CSSOptions(stripWhiteSpace = True, stripComments = True, minimizeValues = True,
                                       stripExtraSemicolons = True, colorize = False, compileScss = True)
        styleSheet = parser.parse(scssContent, options)

        compiler = scsscompiler.SCSSCompiler()
        for prerequisite in module["dependencies"]:
            if self.modules[prerequisite]["scssScope"]:
                self.log("    Including SCSS scope of %s module..." % prerequisite)
                compiler.getGlobalScope().merge(self.modules[prerequisite]["scssScope"])

        compiler.compile(styleSheet, options)
        cssContent = styleSheet.toString(options)

        # inline base64 encoded images
        for fileName in dirEntries(module["dir"] + "/img/base64"):
            b64Image = base64EncodeImage(module["dir"] + "/img/base64/" + fileName)
            cssContent = cssContent.replace("url(base64/%s)" % fileName, "url(%s)" % b64Image)

        module["cssContent"] = cssContent
        module["scssScope"] = compiler.getGlobalScope()

    def loadJavaScript(self, moduleName, module):
        if "jsContent" in module:
            self.log("  Using cached JavaScript...")
            return

        self.log("  Loading JavaScript...")

        jsContent = u"" # all the JavaScript code aggregated into one string
        inlineJs = u"" # special JavaScript that should be inlined

        varReplacer = re.compile(r"^(?:var )?([a-zA-Z0-9.]+) = ", flags = re.MULTILINE)
        functionReplacer = re.compile(r"^function ([a-zA-Z0-9]+)\(", flags = re.MULTILINE)

        for source in module["js"]:
            path = module["dir"] + "/js/" + source["path"]
            if self.options.jslint and (not "runJsLint" in source or source["runJsLint"] == True):
                if not self.jsLint(path):
                    raise BuildError("jslint error in path %s" % path)

            with open(path) as f:
                js = f.read()

                # put all global declarations in the module namespace
                if not "excludeFromNamespace" in source or source["excludeFromNamespace"] == False:
                    js = varReplacer.sub(r"%s.\1 = " % module["namespace"], js)
                    js = functionReplacer.sub(r"%s.\1 = function \1(" % module["namespace"], js)

                js = js.strip()
                js += ("\n" if js[-1] == ";" else ";\n")

                if "inline" in source and source["inline"] == True:
                    if not moduleName == "boot":
                        raise BuildError("Inlining sources is only allowed from the boot module")

                    if module["keepInline"]:
                        jsContent += js
                    else:
                        inlineJs += js
                else:
                    jsContent += js

        # start declaration of the module and begin namespace
        if moduleName == "boot":
            bootNs = module["namespace"]
            bootNsPrefix = ""
            jsContent = ("var %s = %s || {};\n"
                         "with (%s) {\n%s\n") % (bootNs, bootNs, bootNs, jsContent)
        else:
            bootNs = self.modules["boot"]["namespace"]
            moduleNs = module["namespace"]
            if bootNs == moduleNs:
                bootNsPrefix = ""
                jsContent = ("with (%s) {\n"
                             "Modules.%s = {};\n"
                             "%s\n" %
                             (moduleNs, moduleName, jsContent))
            else:
                bootNsPrefix = bootNs + "."
                jsContent = ("%sModules.%s = {};\n"
                             "var %s = %s || {};\n"
                             "with (%s) {\n%s\n" %
                             (bootNsPrefix, moduleNs, moduleNs, moduleNs, moduleNs, jsContent))

        module["jsContent"] = jsContent
        module["inlineJs"] = inlineJs

    def loadTemplates(self, moduleName, module):
        if "templates" in module:
            self.log("  Using cached templates...")
            return

        module["templates"] = u"" # all template definitions (compiled into JavaScript)

        if len(module["tmpl"]) == 0:
            return

        self.log("  Loading templates...")

        bootNs = self.modules["boot"]["namespace"]

        flmlReplacer = re.compile(r"{{flml (.*?)}}", flags = re.MULTILINE)
        i18nReplacer = re.compile(r"{{i18n (.*?)}}", flags = re.MULTILINE)
        wsReplacer = re.compile(r"([>}])[ \t\n\r\f\v]+([<{])", flags = re.MULTILINE)

        for resource in module["tmpl"]:
            path = module["dir"] + "/tmpl/" + resource["path"]
            with open(path) as f:
                template = None
                templateId = None
                for line in f.readlines():
                    if line.startswith("<!-- template"):
                        try:
                            templateId = re.findall(r"id=\"(.*)\"", line)[0]
                        except IndexError:
                            raise BuildError("Template is missing an ID in file %s" % path)
                        template = ""
                    elif line.startswith("<!-- /template"):
                        template = template.replace(' href="#"', " href=\"javascript:void(0)\"")
                        template = template.replace(" href='#'", " href=\"javascript:void(0)\"")

                        template = flmlReplacer.sub("{{html Hyves.FLML.toHTML(\\1)}}", template)
                        template = i18nReplacer.sub("{{html %s.i18n(\"%s\", \\1)}}" % (bootNs, moduleName), template)

                        template = wsReplacer.sub(r"\1 \2", template).strip()

                        module["templates"] += "$.template(\"%s.%s\", %s);\n" % (moduleName, templateId, json.dumps(template))
                        template = None
                    else:
                        if template is not None:
                            template += line

    def loadTranslations(self, module):
        if "translations" in module:
            self.log("  Using cached translations...")
            return

        module["translations"] = {} # these are the module's own translations
        module["allTranslations"] = {} # these are the module's translations, including
                                       # translations from prerequisite modules

        self.log("  Loading translations...")

        for prerequisite in module["dependencies"]:
            module["allTranslations"].update(self.modules[prerequisite]["translations"])

        if len(module["i18n"]) == 0:
            return

        for resource in module["i18n"]:
            path = module["dir"] + "/i18n/" + resource["path"]
            with open(path) as f:
                try:
                    translations = json.load(f)
                    module["translations"].update(translations)
                    module["allTranslations"].update(translations)
                except:
                    raise BuildError("Error in translation file %s" % path)

    def copyStatics(self, module):
        if len(module["statics"]) == 0:
            module["staticMap"] = {}
            return

        self.log("  Copying statics...")

        if "staticMap" in module:
            for path in module["staticMap"]:
                destFileName = module["staticMap"][path]
                destPath = self.targetDir + "/" + destFileName
                if not os.path.exists(destPath): # avoid multiple locales copying the same file
                    shutil.copy(module["dir"] + "/statics/" + path, destPath)
        else:
            module["staticMap"] = {}
            for staticFile in module["statics"]:
                path = staticFile["path"]
                self.log("    %s..." % path)
                (directory, fileName) = os.path.split(path)
                (baseName, extension) = os.path.splitext(fileName)
                with open(module["dir"] + "/statics/" + path, "r") as inFile:
                    content = inFile.read()
                    destFileName = self.getDestinationFileName(module["name"], baseName, content, None, extension)
                destPath = self.targetDir + "/" + destFileName
                if not os.path.exists(destPath): # avoid multiple locales copying the same file
                    with open(destPath, "w") as outFile:
                        outFile.write(content)
                module["staticMap"][path] = destFileName

    def packageJavascript(self, moduleName, module, isDebugFile = False):
        self.log("  Writing %sJavaScript package..." % ("debug " if isDebugFile else ""))

        bootNs = self.modules["boot"]["namespace"]
        if bootNs == module["namespace"]:
            bootNsPrefix = ""
        else:
            bootNsPrefix = bootNs + "."

        js = module["jsContent"]

        # include templates into the JavaScript
        if module["templates"] != "":
            js += module["templates"]

        js = self.removeDevelopmentStuffFromJavaScript(js)

        js = self.applyTranslations(js, module)

        # include CSS into the JavaScript
        if len(module["css"]) > 0:
            js += ("%sModules.%s.css = \"%s\";\n" % (bootNsPrefix, moduleName,
                                                     module["cssContent"].replace("\"", "\\\"").replace("\n", "\\n")))

        # include translations into the JavaScript
        localeTranslations = {}
        for key, values in sorted(module["translations"].items()):
            if self.currentLocale in values:
                value = values[self.currentLocale]
            elif DEFAULT_LOCALE in values:
                value = values[DEFAULT_LOCALE]
            else:
                raise BuildError("No translation possible for key %s and locale %s" % (key, self.currentLocale))
            localeTranslations[key] = value
        js += "%sModules.%s.translations = %s;\n" % (bootNsPrefix, moduleName, json.dumps(localeTranslations))

        # include statics into the JavaScript
        if len(module["staticMap"]) > 0:
            staticMap = []
            for path in module["staticMap"]:
                staticMap.append("\"%s\": \"%s\"" % (path, module["staticMap"][path]))
            js += "%sModules.%s.statics = { %s };\n" % (bootNsPrefix, moduleName, ", ".join(staticMap))

        if self.options.stringifyModules and not module["keepInline"]:
            # close the namespace
            js += "}\n"

            if self.options.minifyJavaScript and not isDebugFile:
                path = self.minifyAndWriteJavaScript(js, moduleName)
                with codecs.open(path, "r", "utf-8") as f:
                    js = f.read()

            if moduleName == "boot":
                js = "try{%s.Modules.addModule(\"%s\",'%s')}catch(e){giveUp(e)}" % (bootNs, moduleName, js.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n"))
            else:
                js = "%s.Modules.addModule(\"%s\",'%s')" % (bootNs, moduleName, js.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n"))
        else:
            js += "%sModules.enableModule(\"%s\");\n" % (bootNsPrefix, moduleName)

            # close the namespace
            js += "}\n"

        (directory, fileName) = os.path.split(self.minifyAndWriteJavaScript(js, moduleName + "-inline" if module["keepInline"] else moduleName, isDebugFile))
        return fileName

    def removeDevelopmentStuffFromJavaScript(self, js):
        if self.options.removeConsoleLog:
            r = re.compile(r".*console.log.*$", flags = re.MULTILINE)
            js = r.sub("", js)

        if self.options.removeLoggingDebug:
            r = re.compile(r".*logging.debug.*$", flags = re.MULTILINE)
            js = r.sub("", js)

        if self.options.removeProfiling:
            r = re.compile(r".*Profiling.(start|stop|enable).*$", flags = re.MULTILINE)
            js = r.sub("", js)

        return js

    def applyTranslations(self, jsContent, module):
        if len(module["allTranslations"]) == 0:
            return jsContent

        self.log("    Applying translations...")

        for match in re.finditer(r"\[\[([a-zA-Z0-9_]+)\]\]", jsContent):
            key = match.group(1)
            if not key in module["allTranslations"]:
                raise BuildError("Undefined text key: %s" % key)

            translations = module["allTranslations"][key]
            if not self.currentLocale in translations:
                raise BuildError("Translation not provided for text key %s and locale %s" % (key, self.currentLocale))
            jsContent = jsContent.replace("[[%s]]" % key, htmlEscape(translations[self.currentLocale]))

        return jsContent

    def minifyAndWriteJavaScript(self, js, moduleName, isDebugFile = False):

        # minify JavaScript (optional) and move to its final location
        if not self.options.minifyJavaScript or isDebugFile:
            versionedPath = self.getDestinationPath(moduleName, None, js, self.currentLocale, "js", isDebugFile)
            with codecs.open(versionedPath, "w", "utf-8") as f:
                f.write(js)
        else:
            uncompiledPath = self.getDestinationPath(moduleName, None, None, self.currentLocale, "js")
            with codecs.open(uncompiledPath, "w", "utf-8") as f:
                f.write(js)

            compiledPath = self.getDestinationPath(moduleName + ".compiled", None, None, self.currentLocale, "js")
            closureOut = self.targetDir + "/closure.out"
            result = os.system("java -jar other/closure-compiler/compiler.jar --js %s --js_output_file %s --jscomp_off nonStandardJsDocs 2>%s" % (uncompiledPath, compiledPath, closureOut))
            if result == 0:
                with codecs.open(compiledPath, "r", "utf-8") as f:
                    versionedPath = self.getDestinationPath(moduleName, None, f.read(), self.currentLocale, "js")
                os.remove(uncompiledPath)
                os.remove(closureOut)
                shutil.move(compiledPath, versionedPath)
            else:
                with codecs.open(closureOut, "r", "utf-8") as f:
                    print f.read()
                raise BuildError("Compilation of %s failed." % uncompiledPath)

        return versionedPath

    def jsLint(self, path):
        self.log("    Running jslint for %s" % path)
        cmd = "other/jslint/lint < %s" % path
        result = os.system(cmd + " > /dev/null")
        if result != 0:
            os.system(cmd) # run again to show report
            return False
        else:
            return True

    def addResource(self, moduleName, resource):
        if not moduleName in self.resources:
            self.resources[moduleName] = {}
        self.resources[moduleName][self.currentLocale] = resource

    def getContentHash(self, content):
        if isinstance(content, unicode):
            return hashlib.md5(content.encode("utf-8")).hexdigest()[:12]
        else:
            return hashlib.md5(content).hexdigest()[:12]

    def getDestinationFileName(self, moduleName, baseName, content, locale, extension, isDebugFile = False):
        path = moduleName or ""
        if baseName != None:
            path += "." + baseName
        if content != None:
            path += "." + self.getContentHash(content)
        if locale != None and locale != "*":
            path += "." + locale
        if isDebugFile:
            path += ".debug"
        if extension.startswith("."):
            path += extension
        else:
            path += "." + extension
        return path

    def getDestinationPath(self, moduleName, baseName, content, locale, extension, isDebugFile = False):
        return self.targetDir + "/" + self.getDestinationFileName(moduleName, baseName, content, locale, extension, isDebugFile)

    def writeDestinationFile(self, moduleName, baseName, extension, contents, isDebugFile = False):
        destPath = self.getDestinationPath(moduleName, baseName, contents, self.currentLocale, extension, isDebugFile)
        with open(destPath, "w") as f:
            f.write(contents)
        return destPath

    def processBootHtml(self, exception = None, isDebugFile = False):
        if not "core" in self.modules:
            exception = BuildError("No module named core has been defined. "
                                   "This module is required to start the actual application.")

        if exception is None:
            bootScriptsJson = []
            manifestList = ""

            resources = copy.deepcopy(self.resources)
            for moduleName in resources:
                module = self.modules[moduleName]
                path = resources[moduleName][self.currentLocale]
                if "excludeFromCache" not in module or module["excludeFromCache"] == False:
                    manifestList += self.options.staticsHost + path + "\n"
                if isDebugFile: # put the debug resources into the resources object
                    path = self.debugResources["boot" if moduleName == "boot-inline" else moduleName][self.currentLocale]
                    resources[moduleName][self.currentLocale] = path
                if moduleName == "boot-inline":
                    bootScriptsJson.append("[static_base]/" + path)

                if self.modules[moduleName]["essential"]:
                    resources[moduleName]["essential"] = True
                if moduleName != "boot-inline":
                    resources[moduleName]["dependencies"] = self.modules[moduleName]["dependencies"]

            if self.options.useCacheManifest:
                with open("boot/cache.manifest.tpl") as f:
                    manifestContents = f.read() % { "resources": manifestList }
                manifestPath = self.getDestinationPath("cache", None, None, self.currentLocale, ".manifest")
                with open(manifestPath, "w") as f:
                    f.write(manifestContents)
                (directory, fileName) = os.path.split(manifestPath)
                manifestUrl = self.options.relativeStaticsPath + fileName
            else:
                manifestUrl = "/does-not-exist.manifest"

            bootResources = copy.deepcopy(resources)
            del bootResources["boot-inline"]
            headJs = (
                "try{document.domain='%(domain)s'}catch(e){}"
                "function giveUp(e){var a=confirm('Het spijt me te moeten zeggen dat %(title)s niet kon opstarten. Zullen we het opnieuw proberen?');"
                r"if(a){window.location.reload()}else{document.body.innerHTML='<h1>'+e+'</h1><p><button onclick=\'window.location.reload()\'>Verfrissen</button></p>'}}"
                "try{"
                "var %(bootNs)s=%(bootNs)s||{};"
                "with(%(bootNs)s){"
                "%(inlineJs)s"
                "%(bootNs)s.go=function(){"
                "Modules.config(\"[static_base]\",\"%(locale)s\",%(resources)s);"
                "Modules.load([\"boot\",\"core\"]).then(function(){"
                "%(coreNs)s.Application.init([config])"
                "})"
                "}"
                "}"
                "}catch(e){giveUp(e)}"
            ) % {
                "domain": DOMAIN,
                "title": TITLE,
                "bootNs": self.modules["boot"]["namespace"],
                "inlineJs": self.modules["boot"]["inlineJs"],
                "coreNs": self.modules["core"]["namespace"],
                "locale": self.currentLocale,
                "resources": json.dumps(bootResources).replace(" ", "")
            }

            if self.options.minifyJavaScript:
                path = self.minifyAndWriteJavaScript(headJs, "inline")
                with codecs.open(path, "r", "utf-8") as f:
                    headJs = f.read()
                os.remove(path)

            onloadJs = "try{%s.go()}catch(e){giveUp(e)}" % self.modules["boot"]["namespace"]

            # icing on the cake, include your favorite icon in the HTML
            favicon = base64EncodeImage("boot/favicon.ico")

            with open("boot/boot.tpl.html") as f:
                bootHtml = f.read() % {
                    "favicon": favicon,
                    "title": TITLE,
                    "manifestUrl": manifestUrl,
                    "headJs": headJs,
                    "onloadJs": onloadJs
                }

            jsonResources = copy.deepcopy(resources)
            jsonResources["boot"] = jsonResources["boot-inline"]
            del jsonResources["boot-inline"]
            bootJson = json.dumps({
                "bootscripts": bootScriptsJson,
                "resources": jsonResources,
                "locale": self.currentLocale,
                "baseurl": "[static_base]/",
                "config": "[config]"
            })
        else:
            bootHtml = ("<!DOCTYPE html>"
                        "<html>"
                            "<head></head>"
                            "<body>"
                                "<h1>Build error: %s</h1>"
                                "<pre>%s</pre>"
                            "</body>"
                        "</html>") % (str(exception), traceback.format_exc())

            bootJson = ""

        destHtmlPath = self.writeDestinationFile("boot", None, "html", bootHtml, isDebugFile)
        destJsonPath = self.writeDestinationFile("boot", None, "json", bootJson, isDebugFile)

        return destHtmlPath, self.getContentHash(bootHtml), destJsonPath, self.getContentHash(bootJson)

    def writeVersionFile(self, name, content, isDebugFile = False):
        #write a pointer to the version of the current boot script
        versionPath = self.getDestinationPath(name, None, None, self.currentLocale, "md5", isDebugFile)
        with open(versionPath, "w") as f:
            f.write(content)

    def zipBuild(self):
        cwd = os.getcwd()
        os.chdir(self.finalTargetDir)
        os.system("tar czf hybrid.tar.gz *")
        shutil.copy("hybrid.tar.gz", cwd)
        os.chdir(cwd)
        self.log("Created hybrid.tar.gz distribution in current directory. Done.")

    def serve(self):
        watcherPid = self.watch()
        if watcherPid:
            signal.signal(signal.SIGINT, stopWatcher)

        # copy cgi-bin dir to tmp build dir, because the CGIHTTPServer can only serve from a single root
        shutil.copytree("cgi-bin", os.path.join(self.tempBuildDir, "cgi-bin"))
        os.system("chmod -R a+rwx " + self.tempBuildDir)

        # set working directory as the CGIHTTPServer will only serve from current dir
        os.chdir(self.tempBuildDir)
        import CGIHTTPServer
        import BaseHTTPServer
        class CustomHandler(CGIHTTPServer.CGIHTTPRequestHandler):
            def do_GET(self):
                #if self.path[-9:] == "manifest":
                #    self.send_header("Cache-Control", "max-age: 9000") # cache 15 min
                CGIHTTPServer.CGIHTTPRequestHandler.do_GET(self)

        handler = CustomHandler
        handler.extensions_map[".manifest"] = "text/cache-manifest"
        httpd = BaseHTTPServer.HTTPServer(("0.0.0.0", 9090), handler)
        httpd.allow_reuse_address = True
        self.log("Serving at http://localhost:9090/cgi-bin/index.py")
        httpd.serve_forever()

    def watch(self):
        if self.options.watch:
            self.log("Auto rebuilding every 1 second")
            pid = os.fork()
            if pid == 0:
                self.watcher() # rebuilds forever

            return pid
        else:
            return 0

    def watcher(self):
        buildNum = 0
        while True:
            time.sleep(1.0)
            buildNum += 1
            sys.stdout.write("\rBuilding build #%s..." % buildNum)
            sys.stdout.flush()
            try:
                self.verbose = False
                self.build()
            except Exception, exception:
                self.verbose = True
                self.log("Exception during build: %s\nRebuilding with verbose output..." % str(exception))
                try:
                    self.build()
                except:
                    pass

def parseOptions():
    usage = "usage: %prog [options]"
    parser = OptionParser(usage = usage)
    parser.add_option("", "--development", action = "store_true",
                      help = "Create a build for development purposes, disables minifying")
    parser.add_option("", "--serve", action = "store_true",
                      help = "Host the application directly from the build directory using the build-in Python webserver")
    parser.add_option("", "--tmp-build-dir")
    parser.add_option("", "--watch", action = "store_true",
                      help = "When self-hosting the application, also watch the modules on disk, and auto-rebuild on change")
    parser.add_option("", "--vm", action = "store_true",
                      help = "Host application from a VM, requires --tmp-build-dir")
    parser.add_option("", "--statics-host",
                      help = "Specify the base URL of a host from where the static files will be hosted")
    parser.add_option("", "--relative-statics-path",
                      help = "Specify the relative path from which the statics can be reached, this will be used instead of the statics host when Same-Origin restrictions apply")
    parser.add_option("", "--stringify-modules", action = "store_true",
                      help = "Wraps all modules except boot as a string to allow localStorage caching and ease certain debugging capabilities (BlackBerry)")
    parser.add_option("", "--no-cache", action = "store_true",
                      help = "Disable cache manifest")

    (options, args) = parser.parse_args()

    class Config:
        minifyJavaScript = True
        createIndexHtml = False
        createZipBuild = True
        serve = False
        watch = False
        jslint = True
        removeConsoleLog = True
        removeLoggingDebug = True
        removeProfiling = True
        tempBuildDir = None
        includeDebugFiles = True
        serveFromVm = False
        staticsHost = ""
        useCacheManifest = True
        stringifyModules = True

    config = Config()

    if options.tmp_build_dir:
        config.tempBuildDir = options.tmp_build_dir

    if options.development:
        config.minifyJavaScript = False
        config.createIndexHtml = True
        config.createZipBuild = False
        config.jslint = False
        config.removeConsoleLog = False
        config.removeLoggingDebug = False
        config.removeProfiling = False
        config.includeDebugFiles = False # files are already stripped in dev mode
        config.stringifyModules = False
        config.staticsHost = "/build/"

    if options.statics_host:
        config.staticsHost = options.statics_host

    if options.relative_statics_path:
        config.relativeStaticsPath = options.relative_statics_path
    else:
        config.relativeStaticsPath = config.staticsHost

    if options.stringify_modules:
        config.stringifyModules = True

    if options.no_cache:
        config.useCacheManifest = False

    if options.vm:
        config.serveFromVm = True
        config.watch = True
        if not options.tmp_build_dir:
            print "Please specify the path to the hybrid dir of mainweb, e.g. make serve_vm BUILDDIR=/Users/emiel/Work/trunk/root/statics/hybrid/"
            exit(1)

    if options.serve:
        config.serve = True

    if options.watch:
        config.watch = True

    return config

def main():
    builder = Builder(parseOptions())

    builder.build()

    if builder.options.serve:
        builder.serve()

if __name__ == "__main__":
    os.stat_float_times(True)
    main()
